using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Data;
using Dorm.Maps;

namespace Dorm.Services
{
    public class DbToEntityMapper
    {
        public bool IncludeAlias { get; set; }

        public IList Map(Type entityType, IList entityList, IDataReader dataReader)
        {
            if (!dataReader.IsClosed)
            {
                try
                {
                    while (dataReader.Read())
                    {
                        List<MappedProperty> entityMaps;
                        var entity = Convert.ChangeType(FillEntity(entityType, dataReader, out entityMaps, entityType.Name), entityType);
                        entityList.Add(entity);
                    }
                    dataReader.Close();
                }
                catch (Exception)
                {
                    dataReader.Close();
                    throw;
                }
            }

            dataReader.Dispose();

            return entityList;
        }

        private object FillEntity(Type type, IDataReader dataReader, out List<MappedProperty> mappedProperties, string alias)
        {
            var entity = FillEntityScalars(type, dataReader, out mappedProperties, alias);
            FillEntityReferences(dataReader, mappedProperties, entity);
            FillEntityComponents(alias, dataReader, mappedProperties, entity);

            return entity;
        }

        private object FillEntityScalars(Type type, IDataReader dataReader, out List<MappedProperty> mappedProperties, string alias)
        {
            mappedProperties = MapLocator.GetMap(type);
            object entity = Activator.CreateInstance(type);

            string idName = IncludeAlias ? alias + "_ID" : "ID";
            
            if (dataReader[idName] == DBNull.Value)
                return null;

            type.GetProperty("ID").SetValue(entity, dataReader[idName], null);

            // Entity Scalar
            foreach (var mappedProperty in mappedProperties.Where(m => m.MappingType == MappedProperty.MappingTypes.Simple))
            {
                string columnName = IncludeAlias ? alias + "_" + mappedProperty.PropertyName : mappedProperty.PropertyName;
                var propertyValue = dataReader[columnName];
                if (propertyValue != null && propertyValue != DBNull.Value)
                {
                    var propertyType = type.GetProperty(mappedProperty.PropertyName);
                    if (propertyType != null)
                        propertyType.SetValue(entity, propertyValue, null);
                }                    
            }

            return entity;
        }

        private void FillEntityReferences(IDataReader dataReader, List<MappedProperty> mappedProperties, object entity)
        {
            foreach (var join in mappedProperties.Where(m => m.MappingType == MappedProperty.MappingTypes.Reference))
            {
                if (dataReader.GetSchemaTable().Select("ColumnName Like '" + join.PropertyName + "_%'").Length == 0)
                    continue;

                List<MappedProperty> joinEntityMaps;
                object joinEntity = Convert.ChangeType(FillEntityScalars(join.PropertyType, dataReader, out joinEntityMaps, join.PropertyName), join.PropertyType);

                if (joinEntity != null)
                    entity.GetType().GetProperty(join.PropertyName).SetValue(entity, joinEntity, null);
            }
        }

        private void FillEntityComponents(string alias, IDataReader dataReader, List<MappedProperty> mappedProperties, object entity)
        {
            foreach (var componentMappedEntity in mappedProperties.Where(m => m.MappingType == MappedProperty.MappingTypes.Component))
            {
                if (dataReader.GetSchemaTable().Select("ColumnName Like '%" + componentMappedEntity.PropertyName + "_%'").Length == 0)
                    continue;

                var componentProperties = MapLocator.GetMap(componentMappedEntity.PropertyType);
                var componentValueObject = Activator.CreateInstance(componentMappedEntity.PropertyType);

                // Scalar
                foreach (var componentProperty in componentProperties.Where(p => p.MappingType == MappedProperty.MappingTypes.Simple))
                {
                    string columnName = IncludeAlias ? alias + "_" + componentMappedEntity.PropertyName + "_" + componentProperty.PropertyName
                                            : componentMappedEntity.PropertyName + "_" + componentProperty.PropertyName;

                    var propertyValue = dataReader[columnName];

                    if (propertyValue != null && propertyValue != DBNull.Value)
                    {
                        var propertyType = componentMappedEntity.PropertyType.GetProperty(componentProperty.PropertyName);
                        if (propertyType != null)
                            propertyType.SetValue(componentValueObject, propertyValue, null);
                    }
                }

                // Component References
                foreach (var join in componentProperties.Where(m => m.MappingType == MappedProperty.MappingTypes.Reference))
                {
                    string joinAlias = componentMappedEntity.PropertyName + "_" + join.PropertyName;
                    if (dataReader.GetSchemaTable().Select("ColumnName Like '" + joinAlias + "_%'").Length == 0)
                        continue;

                    List<MappedProperty> joinEntityMaps;
                    object joinEntity = Convert.ChangeType(FillEntity(join.PropertyType, dataReader, out joinEntityMaps, joinAlias), join.PropertyType);

                    if (joinEntity != null)
                        componentValueObject.GetType().GetProperty(join.PropertyName).SetValue(componentValueObject, joinEntity, null);
                }

                entity.GetType().GetProperty(componentMappedEntity.PropertyName)
                    .SetValue(entity, componentValueObject, null);
            }
        }

    }
}
